package com.chinathinks.core.net.ntp;

import java.net.DatagramPacket;

/***
 * Implementation of NtpV3Packet with methods converting Java objects to/from
 * the Network Time Protocol (NTP) data message header format described in
 * RFC-1305.
 * 
 * @author Naz Irizarry, MITRE Corp
 * @author Jason Mathews, MITRE Corp
 * 
 * @version $Revision: 165675 $ $Date: 2005-05-02 15:09:55 -0500 (Mon, 02 May
 *          2005) $
 */
public class NtpV3Impl implements NtpV3Packet {

	private static final int MODE_INDEX = 0;
	private static final int MODE_SHIFT = 0;

	private static final int VERSION_INDEX = 0;
	private static final int VERSION_SHIFT = 3;

	private static final int LI_INDEX = 0;
	private static final int LI_SHIFT = 6;

	private static final int STRATUM_INDEX = 1;
	private static final int POLL_INDEX = 2;
	private static final int PRECISION_INDEX = 3;

	private static final int ROOT_DELAY_INDEX = 4;
	private static final int ROOT_DISPERSION_INDEX = 8;
	private static final int REFERENCE_ID_INDEX = 12;

	private static final int REFERENCE_TIMESTAMP_INDEX = 16;
	private static final int ORIGINATE_TIMESTAMP_INDEX = 24;
	private static final int RECEIVE_TIMESTAMP_INDEX = 32;
	private static final int TRANSMIT_TIMESTAMP_INDEX = 40;

	private static final int KEY_IDENTIFIER_INDEX = 48;
	private static final int MESSAGE_DIGEST = 54; /* len 16 bytes */

	private byte[] buf = new byte[48];

	private DatagramPacket dp;

	/** Creates a new instance of NtpV3Impl */
	public NtpV3Impl() {
	}

	/***
	 * Returns mode as defined in RFC-1305 which is a 3-bit integer whose value
	 * is indicated by the MODE_xxx parameters.
	 * 
	 * @return mode as defined in RFC-1305.
	 */
	public int getMode() {
		return (ui(buf[MODE_INDEX]) >> MODE_SHIFT) & 0x7;
	}

	/***
	 * Return human-readable name of message mode type as described in RFC 1305.
	 * 
	 * @return mode name as string.
	 */
	public String getModeName() {
		return NtpUtils.getModeName(getMode());
	}

	/***
	 * Set mode as defined in RFC-1305.
	 * 
	 * @param mode
	 */
	public void setMode(int mode) {
		buf[MODE_INDEX] = (byte) (buf[MODE_INDEX] & 0xF8 | mode & 0x7);
	}

	/***
	 * Returns leap indicator as defined in RFC-1305 which is a two-bit code:
	 * 0=no warning 1=last minute has 61 seconds 2=last minute has 59 seconds
	 * 3=alarm condition (clock not synchronized)
	 * 
	 * @return leap indicator as defined in RFC-1305.
	 */
	public int getLeapIndicator() {
		return (ui(buf[LI_INDEX]) >> LI_SHIFT) & 0x3;
	}

	/***
	 * Set leap indicator as defined in RFC-1305.
	 * 
	 * @param li
	 *            leap indicator.
	 */
	public void setLeapIndicator(int li) {
		buf[LI_INDEX] = (byte) (buf[LI_INDEX] & 0x3F | ((li & 0x3) << LI_SHIFT));
	}

	/***
	 * Returns poll interval as defined in RFC-1305, which is an eight-bit
	 * signed integer indicating the maximum interval between successive
	 * messages, in seconds to the nearest power of two (e.g. value of six
	 * indicates an interval of 64 seconds. The values that can appear in this
	 * field range from NTP_MINPOLL to NTP_MAXPOLL inclusive.
	 * 
	 * @return poll interval as defined in RFC-1305.
	 */
	public int getPoll() {
		return (int) (buf[POLL_INDEX]);
	}

	/***
	 * Set poll interval as defined in RFC-1305.
	 * 
	 * @param poll
	 *            poll interval.
	 */
	public void setPoll(int poll) {
		buf[POLL_INDEX] = (byte) (poll & 0xFF);
	}

	/***
	 * Returns precision as defined in RFC-1305 encoded as an 8-bit signed
	 * integer (seconds to nearest power of two). Values normally range from -6
	 * to -20.
	 * 
	 * @return precision as defined in RFC-1305.
	 */
	public int getPrecision() {
		return (int) buf[PRECISION_INDEX];
	}

	/***
	 * Set precision as defined in RFC-1305.
	 * 
	 * @param precision
	 */
	public void setPrecision(int precision) {
		buf[PRECISION_INDEX] = (byte) (precision & 0xFF);
	}

	/***
	 * Returns NTP version number as defined in RFC-1305.
	 * 
	 * @return NTP version number.
	 */
	public int getVersion() {
		return (ui(buf[VERSION_INDEX]) >> VERSION_SHIFT) & 0x7;
	}

	/***
	 * Set NTP version as defined in RFC-1305.
	 * 
	 * @param version
	 *            NTP version.
	 */
	public void setVersion(int version) {
		buf[VERSION_INDEX] = (byte) (buf[VERSION_INDEX] & 0xC7 | ((version & 0x7) << VERSION_SHIFT));
	}

	/***
	 * Returns Stratum as defined in RFC-1305, which indicates the stratum level
	 * of the local clock, with values defined as follows: 0=unspecified,
	 * 1=primary ref clock, and all others a secondary reference (via NTP).
	 * 
	 * @return Stratum level as defined in RFC-1305.
	 */
	public int getStratum() {
		return ui(buf[STRATUM_INDEX]);
	}

	/***
	 * Set stratum level as defined in RFC-1305.
	 * 
	 * @param stratum
	 *            stratum level.
	 */
	public void setStratum(int stratum) {
		buf[STRATUM_INDEX] = (byte) (stratum & 0xFF);
	}

	/***
	 * Return root delay as defined in RFC-1305, which is the total roundtrip
	 * delay to the primary reference source, in seconds. Values can take
	 * positive and negative values, depending on clock precision and skew.
	 * 
	 * @return root delay as defined in RFC-1305.
	 */
	public int getRootDelay() {
		return getInt(ROOT_DELAY_INDEX);
	}

	/***
	 * Return root delay as defined in RFC-1305 in milliseconds, which is the
	 * total roundtrip delay to the primary reference source, in seconds. Values
	 * can take positive and negative values, depending on clock precision and
	 * skew.
	 * 
	 * @return root delay in milliseconds
	 */
	public double getRootDelayInMillisDouble() {
		double l = getRootDelay();
		return l / 65.536;
	}

	/***
	 * Returns root dispersion as defined in RFC-1305.
	 * 
	 * @return root dispersion.
	 */
	public int getRootDispersion() {
		return getInt(ROOT_DISPERSION_INDEX);
	}

	/***
	 * Returns root dispersion (as defined in RFC-1305) in milliseconds.
	 * 
	 * @return root dispersion in milliseconds
	 */
	public long getRootDispersionInMillis() {
		long l = getRootDispersion();
		return (l * 1000) / 65536L;
	}

	/***
	 * Returns root dispersion (as defined in RFC-1305) in milliseconds as
	 * double precision value.
	 * 
	 * @return root dispersion in milliseconds
	 */
	public double getRootDispersionInMillisDouble() {
		double l = getRootDispersion();
		return l / 65.536;
	}

	/***
	 * Set reference clock identifier field with 32-bit unsigned integer value.
	 * See RFC-1305 for description.
	 * 
	 * @param refId
	 *            reference clock identifier.
	 */
	public void setReferenceId(int refId) {
		for (int i = 3; i >= 0; i--) {
			buf[REFERENCE_ID_INDEX + i] = (byte) (refId & 0xff);
			refId >>>= 8; // shift right one-byte
		}
	}

	/***
	 * Returns the reference id as defined in RFC-1305, which is a 32-bit
	 * integer whose value is dependent on several criteria.
	 * 
	 * @return the reference id as defined in RFC-1305.
	 */
	public int getReferenceId() {
		return getInt(REFERENCE_ID_INDEX);
	}

	/***
	 * Returns the reference id string. String cannot be null but value is
	 * dependent on the version of the NTP spec supported and stratum level.
	 * Value can be an empty string, clock type string, IP address, or a hex
	 * string.
	 * 
	 * @return the reference id string.
	 */
	public String getReferenceIdString() {
		int version = getVersion();
		int stratum = getStratum();
		if (version == VERSION_3 || version == VERSION_4) {
			if (stratum == 0 || stratum == 1) {
				return idAsString(); // 4-character ASCII string (e.g. GPS,
										// USNO)
			}
			// in NTPv4 servers this is latest transmit timestamp of ref source
			if (version == VERSION_4)
				return idAsHex();
		}

		// Stratum 2 and higher this is a four-octet IPv4 address
		// of the primary reference host.
		if (stratum >= 2) {
			return idAsIPAddress();
		}
		return idAsHex();
	}

	/***
	 * Returns Reference id as dotted IP address.
	 * 
	 * @return refId as IP address string.
	 */
	private String idAsIPAddress() {
		return ui(buf[REFERENCE_ID_INDEX]) + "." + ui(buf[REFERENCE_ID_INDEX + 1]) + "."
				+ ui(buf[REFERENCE_ID_INDEX + 2]) + "." + ui(buf[REFERENCE_ID_INDEX + 3]);
	}

	private String idAsString() {
		String id = "";
		for (int i = 0; i <= 3; i++) {
			char c = (char) buf[REFERENCE_ID_INDEX + i];
			if (c == 0)
				break; // 0-terminated string
			id = id + c;
		}
		return id;
	}

	private String idAsHex() {
		return Integer.toHexString(getReferenceId());
	}

	/***
	 * Returns the transmit timestamp as defined in RFC-1305.
	 * 
	 * @return the transmit timestamp as defined in RFC-1305. Never returns a
	 *         null object.
	 */
	public TimeStamp getTransmitTimeStamp() {
		return getTimestamp(TRANSMIT_TIMESTAMP_INDEX);
	}

	/***
	 * Set transmit time with NTP timestamp. If <code>ts</code> is null then
	 * zero time is used.
	 * 
	 * @param ts
	 *            NTP timestamp
	 */
	public void setTransmitTime(TimeStamp ts) {
		setTimestamp(TRANSMIT_TIMESTAMP_INDEX, ts);
	}

	/***
	 * Set originate timestamp given NTP TimeStamp object. If <code>ts</code> is
	 * null then zero time is used.
	 * 
	 * @param ts
	 *            NTP timestamp
	 */
	public void setOriginateTimeStamp(TimeStamp ts) {
		setTimestamp(ORIGINATE_TIMESTAMP_INDEX, ts);
	}

	/***
	 * Returns the originate time as defined in RFC-1305.
	 * 
	 * @return the originate time. Never returns null.
	 */
	public TimeStamp getOriginateTimeStamp() {
		return getTimestamp(ORIGINATE_TIMESTAMP_INDEX);
	}

	/***
	 * Returns the reference time as defined in RFC-1305.
	 * 
	 * @return the reference time as <code>TimeStamp</code> object. Never
	 *         returns null.
	 */
	public TimeStamp getReferenceTimeStamp() {
		return getTimestamp(REFERENCE_TIMESTAMP_INDEX);
	}

	/***
	 * Set Reference time with NTP timestamp. If <code>ts</code> is null then
	 * zero time is used.
	 * 
	 * @param ts
	 *            NTP timestamp
	 */
	public void setReferenceTime(TimeStamp ts) {
		setTimestamp(REFERENCE_TIMESTAMP_INDEX, ts);
	}

	/***
	 * Returns receive timestamp as defined in RFC-1305.
	 * 
	 * @return the receive time. Never returns null.
	 */
	public TimeStamp getReceiveTimeStamp() {
		return getTimestamp(RECEIVE_TIMESTAMP_INDEX);
	}

	/***
	 * Set receive timestamp given NTP TimeStamp object. If <code>ts</code> is
	 * null then zero time is used.
	 * 
	 * @param ts
	 *            timestamp
	 */
	public void setReceiveTimeStamp(TimeStamp ts) {
		setTimestamp(RECEIVE_TIMESTAMP_INDEX, ts);
	}

	/***
	 * Return type of time packet. The values (e.g. NTP, TIME, ICMP, ...)
	 * correspond to the protocol used to obtain the timing information.
	 * 
	 * @return packet type string identifier which in this case is "NTP".
	 */
	public String getType() {
		return "NTP";
	}

	/***
	 * @return 4 bytes as 32-bit int
	 */
	private int getInt(int index) {
		int i = ui(buf[index]) << 24 | ui(buf[index + 1]) << 16 | ui(buf[index + 2]) << 8 | ui(buf[index + 3]);

		return i;
	}

	/***
	 * Get NTP Timestamp at specified starting index.
	 * 
	 * @param index
	 *            index into data array
	 * @return TimeStamp object for 64 bits starting at index
	 */
	private TimeStamp getTimestamp(int index) {
		return new TimeStamp(getLong(index));
	}

	/***
	 * Get Long value represented by bits starting at specified index.
	 * 
	 * @return 8 bytes as 64-bit long
	 */
	private long getLong(int index) {
		long i = ul(buf[index]) << 56 | ul(buf[index + 1]) << 48 | ul(buf[index + 2]) << 40 | ul(buf[index + 3]) << 32
				| ul(buf[index + 4]) << 24 | ul(buf[index + 5]) << 16 | ul(buf[index + 6]) << 8 | ul(buf[index + 7]);
		return i;
	}

	/***
	 * Sets the NTP timestamp at the given array index.
	 * 
	 * @param index
	 *            index into the byte array.
	 * @param t
	 *            TimeStamp.
	 */
	private void setTimestamp(int index, TimeStamp t) {
		long ntpTime = (t == null) ? 0 : t.ntpValue();
		// copy 64-bits from Long value into 8 x 8-bit bytes of array
		// one byte at a time shifting 8-bits for each position.
		for (int i = 7; i >= 0; i--) {
			buf[index + i] = (byte) (ntpTime & 0xFF);
			ntpTime >>>= 8; // shift to next byte
		}
		// buf[index] |= 0x80; // only set if 1900 baseline....
	}

	/***
	 * Returns the datagram packet with the NTP details already filled in.
	 * 
	 * @return a datagram packet.
	 */
	public DatagramPacket getDatagramPacket() {
		if (dp == null)
			synchronized (this) {
				if (dp == null) {
					dp = new DatagramPacket(buf, buf.length);
					dp.setPort(NTP_PORT);
				}
			}
		return dp;
	}

	/***
	 * Set the contents of this object from source datagram packet.
	 * 
	 * @param srcDp
	 *            source DatagramPacket to copy contents from.
	 */
	public void setDatagramPacket(DatagramPacket srcDp) {
		byte[] incomingBuf = srcDp.getData();
		int len = srcDp.getLength();
		if (len > buf.length)
			len = buf.length;

		System.arraycopy(incomingBuf, 0, buf, 0, len);
	}

	/***
	 * Convert byte to unsigned integer. Java only has signed types so we have
	 * to do more work to get unsigned ops.
	 * 
	 * @param b
	 * @return unsigned int value of byte
	 */
	protected final static int ui(byte b) {
		int i = b & 0xFF;
		return i;
	}

	/***
	 * Convert byte to unsigned long. Java only has signed types so we have to
	 * do more work to get unsigned ops
	 * 
	 * @param b
	 * @return unsigned long value of byte
	 */
	protected final static long ul(byte b) {
		long i = b & 0xFF;
		return i;
	}

	/***
	 * Returns details of NTP packet as a string.
	 * 
	 * @return details of NTP packet as a string.
	 */
	public String toString() {
		return "[" + "version:" + getVersion() + ", mode:" + getMode() + ", poll:" + getPoll() + ", precision:"
				+ getPrecision() + ", delay:" + getRootDelay() + ", dispersion(ms):"
				+ getRootDispersionInMillisDouble() + ", id:" + getReferenceIdString() + ", xmitTime:"
				+ getTransmitTimeStamp().toDateString() + " ]";
	}

}
